package hk.ust.comp3111.ustnutrition.data;

import hk.ust.comp3111.ustnutrition.data.db.AppConfig;
import hk.ust.comp3111.ustnutrition.data.db.Food;
import hk.ust.comp3111.ustnutrition.data.db.IntakeRecord;
import hk.ust.comp3111.ustnutrition.data.db.Restaurant;
import hk.ust.comp3111.ustnutrition.data.db.User;
import hk.ust.comp3111.ustnutrition.data.json.ApiResponseBase;
import hk.ust.comp3111.ustnutrition.data.json.ApiResponseRank;
import hk.ust.comp3111.ustnutrition.data.json.ApiResponseStatistics;
import hk.ust.comp3111.ustnutrition.data.json.ApiResponseValidate;
import hk.ust.comp3111.ustnutrition.formula.ScoreCalculator;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Observable;

import org.apache.http.entity.StringEntity;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.util.Log;

import com.loopj.android.http.*;
import com.couchbase.lite.*;

public class DataAccessor extends Observable {
	static final String TAG = "hk.ust.comp3111.ustnutrition.DataAccessor";
	
	public static final String API_HOST = "http://ust-nutrition.hopto.org:5000/api";
	public static final String RES_HOST = "http://ust-nutrition.hopto.org:5000/res/";
	public static final String DATA_HOST = "http://ust-nutrition.hopto.org:4984";
	
	public static final DataAccessor Instance = new DataAccessor();
	AsyncHttpClient client = new AsyncHttpClient();
	Context context;
	Manager manager;
	Map<Class<?>, DataTable<?>> tables = new HashMap<Class<?>, DataTable<?>>();
	
	boolean offlineMode;
	
	private DataAccessor()
	{
	}
	
	public <T extends DataRow> DataTable<T> getTable(Class<T> fieldClass)
	{
		@SuppressWarnings("unchecked")
		DataTable<T> table = (DataTable<T>) tables.get(fieldClass);
		if(table == null)
			throw new RuntimeException("Cannot find table " + fieldClass.getName());
		return table;
	}
	
	public <T extends DataRow> DataTable<T> createTable(Class<T> tableClass)
	{
		return createTable(tableClass, tableClass);
	}
	
	public <T extends DataRow> DataTable<T> createTable(Class<T> tableClass, Class<T> rowClass)
	{
		DataTableName name = tableClass.getAnnotation(DataTableName.class);
		URL url = null;
		if(name == null)
			throw new RuntimeException("Cannot find DataTableName annotation from the class");
		if(name.sync()) {
			try {
				url = new URL(new URL(DATA_HOST), "/" + name.value());
			} catch (MalformedURLException e) {
				Log.e(TAG, "Cannot create url");
			}
		}
		return createTable(tableClass, rowClass, url);
	}
	
	public <T extends DataRow> DataTable<T> createTable(Class<T> tableClass, Class<T> rowClass, URL syncUrl)
	{
		@SuppressWarnings("unchecked")
		DataTable<T> table = (DataTable<T>) tables.get(tableClass);
		if(table == null)
			tables.put(tableClass, table = new DataTable<T>(this, tableClass, rowClass, syncUrl));
		return table;
	}
	
	public void init(Context mcontext)
	{
		if(context != null) return;
		context = mcontext;
		try {
		     manager = new Manager(mcontext.getFilesDir(), Manager.DEFAULT_OPTIONS);
		     configs = createTable(AppConfig.class, null);
		     user = configs.get("user", User.class, true);
		     restaurants =  createTable(Restaurant.class);
		     foods = createTable(Food.class);
		     if(foods.isEmpty()) {
		    	 //TODO ask for download data
		     }
		     records = createTable(IntakeRecord.class);
		 } catch (IOException e) {
		     Log.e(TAG, "Cannot create manager object");
		     return;
		 }
	}
	
	DataTable<AppConfig> configs;
	DataTable<Food> foods;
	DataTable<Restaurant> restaurants;
	DataTable<IntakeRecord> records;
	User user;
	
	public DataTable<AppConfig> getConfigs() {
		return configs;
	}
	
	public DataTable<Food> getFoods() {
		return foods;
	}
	
	public DataTable<Restaurant> getRestaurants() {
		return restaurants;
	}
	
	public DataTable<IntakeRecord> getRecords() {
		return records;
	}
	
	public User getUser() {
		return user;
	}
	
	public Context getContext() {
		return context;
	}
	
	public Manager getManager() {
		return manager;
	}
	
	public boolean getOfflineMode() {
		return offlineMode;
	}
	
	public void setOfflineMode(boolean offlineMode) {
		this.offlineMode = offlineMode;
	}
	
	public <T extends ApiResponseBase> void Api(final JSONObject request, final ApiHandler<T> handler, final Class<T> responseClass, final int retryCount)
	{
		StringEntity entity;
		try {
			entity = new StringEntity(request.toString());
			client.post(null, API_HOST, entity, "application/json",  new JsonHttpResponseHandler() {
	            @Override
	            public void onSuccess(JSONObject obj) {
	            	try
	            	{
	            		if(handler != null) {
	            			ApiResponseBase base = new ApiResponseBase();
	            			base.load(obj);
		            		if(base.getCode() < 0)
		            		{
		            			handler.onFailure(base.getMessage());
		            		}
		            		else
		            		{
			            		T response = responseClass.newInstance();
			            		response.load(obj);
		            			handler.onSuccess(response);
		            		}
	            		}
	            	}
	            	catch(JSONException exc)
	            	{
	            		if(handler != null)
	            			handler.onFailure("Invalid response: " + exc.toString());
	            	} catch (InstantiationException e) {
	            		throw new RuntimeException("Cannot create " + responseClass.getName());
					} catch (IllegalAccessException e) {
	            		throw new RuntimeException("Cannot create " + responseClass.getName());
					}
	            }
	            
	            @Override
	            public void onFailure(Throwable exc, JSONObject obj) {
	            	if(retryCount > 0)
	            		Api(request, handler, responseClass, retryCount - 1);
	            	else
	            	{
	            		if(handler != null)
	            			handler.onFailure(exc.toString());
	            	}
	            }
	        });
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
	}

	public String getResource(String name) {
		if(name == null) return null;
		return RES_HOST + name + ".jpg";
	}
	
	public void login(final String email, final ApiHandler<ApiResponseBase> handler) {
		JSONObject obj = new JSONObject();
		try {
			obj.put("cmd", "login");
			obj.put("email", email);
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		
		Api(obj, new ApiHandler<ApiResponseBase>() {
			@Override
			public void onSuccess(ApiResponseBase response) {
				if(handler != null)
					handler.onSuccess(response);
				DataAccessor.this.getUser().setEmail(email);
			}
			
			public void onFailure(String message) {
				if(handler != null)
					handler.onFailure(message);
			}
		}, ApiResponseBase.class, 5);
	}
	
	public void validate(final String email, String code, final ApiHandler<ApiResponseBase> handler) {
		JSONObject obj = new JSONObject();
		try {
			obj.put("cmd", "validate");
			obj.put("code", code);
			obj.put("email", email);
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		
		Api(obj, new ApiHandler<ApiResponseValidate>() {
			@Override
			public void onSuccess(ApiResponseValidate response) {
				if(handler != null)
					handler.onSuccess(response);
				User user = DataAccessor.this.getUser();
				user.login(email, response.getData());
			}
			
			public void onFailure(String message) {
				if(handler != null)
					handler.onFailure(message);
			}
		}, ApiResponseValidate.class, 5);
	}
	
	public void rate(Food food, int score, ApiHandler<ApiResponseBase> handler) {
		JSONObject obj = new JSONObject();
		try {
			obj.put("cmd", "rate");
			obj.put("food", food.getId());
			obj.put("score", score);
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		
		Api(obj, handler, ApiResponseBase.class, 5);
	}
	
	public void rank(ApiHandler<ApiResponseRank> handler) {
		JSONObject obj = new JSONObject();
		try {
			obj.put("cmd", "rank");
			if(getUser().isJoinRanking())
				obj.put("user", this.getUser().getITSC());
			
			//TODO fix this
			obj.put("name", this.getUser().getName());
			ScoreCalculator cal = new ScoreCalculator(getUser());
			obj.put("score", cal.getWeeklyScore());
			
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		
		Api(obj, handler, ApiResponseRank.class, 5);
	}
	
	public void statistics(ApiHandler<ApiResponseStatistics> handler) {
		JSONObject obj = new JSONObject();
		try {
			obj.put("cmd", "statistics");
			
			if(getUser().isJoinRanking()) {
				ScoreCalculator cal = new ScoreCalculator(getUser());
				obj.put("score", cal.getWeeklyScore());
			}
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		
		Api(obj, handler, ApiResponseStatistics.class, 5);
	}

	public void beginUpdateChannel() {
		for(DataTable<?> table : tables.values())
			table.stopSync();
	}

	public void endUpdateChannel() {
		for(DataTable<?> table : tables.values())
			table.sync();
	}
}



